package com.redhipps.hips.client.widget;

import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Widget;
import com.redhipps.hips.client.command.HipsCommand;
import com.redhipps.hips.client.controller.EditCallback;
import com.redhipps.hips.client.controller.ModelEditCallback;
import com.redhipps.hips.client.controller.ModelEditDelegate;
import com.redhipps.hips.client.model.Context;
import com.redhipps.hips.client.model.Model;


/**
 * Table that lists models with formatted attributes per column. Each model
 * occupies one row. Also provides facilities to edit the models in the list
 * through a {@link ModelListEditDelegate} that subclasses can provide.
 * <p>
 * Note that in general the values in this list are NOT kept in sync with the
 * models in the underlying datastore in the App Engine. The edit listeners
 * make a best effort to synchronize the displayed data but there could be
 * concurrency issues.
 * 
 * TODO Investigate concurrency issues.
 * 
 * @author yuhong
 */
abstract public class ModelList<T extends Model> extends Grid {

  private Context context;
  private List<T> values;
  private SelectionListener<T> selectionListener;
  private ModelEditDelegate<T> editingDelgate;

  public ModelList(Context ctx, List<T> values) {
    this.context = ctx;
    setValues(values, false);
  }

  // Attributes

  public List<T> values() {
    return values;
  }

  protected Context context() {
    return context;
  }

  public void setValues(List<T> values) {
    setValues(values, true);
  }

  protected void setValues(List<T> values, boolean refresh) {
    // TODO More columns
    // TODO Sorting
    this.values = values;
    if (refresh) {
      refresh();
    }
  }

  // Formatting table.

  public void refresh() {
    int extraRows = 1;
    if (editingDelgate != null) {
      extraRows += 1;
    }
    int rows = values.size() + extraRows;
    Object[] headers = headers();
    resize(rows, headers.length);
    writeRow(headers, 0);
    for (int i = 0; i < values.size(); i++) {
      T value = values.get(i);
      Object[] rowValues = writeModel(value, i);
      writeRow(rowValues, i + 1);
    }
    if (editingDelgate != null) {
      Button createButton = new Button("Add...");
      createButton.addClickListener(createModelAddClickListener());
      setWidget(rows - 1, 0, createButton);
    }
  }

  /**
   * @return Headers for the model list. Should correspond to the number
   *     of columns in the table.
   */
  abstract protected Object[] headers();

  abstract protected Object[] writeModel(T model, int row);

  protected void writeRow(Object[] rowValues, int row) {
    for (int i = 0; i < rowValues.length; i++) {
      Object value = rowValues[i];
      if (value == null) {
        continue;
      } else if (value instanceof Widget) {
        setWidget(row, i, (Widget) value);
      } else {
        setText(row, i, value.toString());
      }
    }
  }

  // User interaction with the list.

  public void setItemSelectionListener(SelectionListener<T> l) {
    selectionListener = l;
  }

  // Editing the list.

  public void setEditingDelegate(ModelEditDelegate<T> delegate) {
    this.editingDelgate = delegate;
  }

  protected ClickListener createModelAddClickListener() {
    return new ClickListener() {
      public void onClick(Widget sender) {
        editingDelgate.createModel(context, new DefaultAddCallback());
      }
    };
  }

  protected ClickListener createModelEditClickListener(final T model) {
    return new ClickListener() {
      public void onClick(Widget sender) {
        editingDelgate.editModel(context, model, new DefaultEditCallback());
      }
    };
  }

  protected ClickListener createModelDeleteClickListener(final T model) {
    return new ClickListener() {
      public void onClick(Widget sender) {
        editingDelgate.deleteModel(context, model, new DefaultDeleteCallback());
      }
    };
  }

  protected ClickListener createModelSelectionClickListener(final T model) {
    return new ClickListener() {
      public void onClick(Widget sender) {
        if (selectionListener != null) {
          selectionListener.itemSelected(model, sender);
        }
      }
    };
  }

  protected static <T extends Model> void invokeWithEditCallback(
      HipsCommand<T> command, Context context, final EditCallback<T> callback) {
    AsyncCallback<T> wrapperCallback = new AsyncCallback<T>() {
      public void onFailure(Throwable caught) {
        callback.onFailure(caught);
      }
      public void onSuccess(T result) {
        if (result != null) {
          callback.onSuccess(result);
        } else {
          callback.onCancel();
        }
      }
    };
    command.execute(wrapperCallback);
  }

  // Edit callbacks.

  protected class DefaultAddCallback implements ModelEditCallback<T> {
    public void onFailure(Throwable caught) {
      GWT.log("Model edit failed.", caught);
    }
    public void onSuccess(T result) {
      values.add(result);
      refresh();
    }
    public void onCancel() {
      // no-op
    }
  }

  protected class DefaultEditCallback implements ModelEditCallback<T> {
    public void onFailure(Throwable caught) {
      GWT.log("Model edit failed.", caught);
    }
    public void onSuccess(T result) {
      refresh();
    }
    public void onCancel() {
      // no-op
    }
  }

  protected class DefaultDeleteCallback implements ModelEditCallback <T>{
    public void onFailure(Throwable caught) {
      GWT.log("Model edit failed.", caught);
    }
    public void onSuccess(T result) {
      values.remove(result);
      refresh();
    }
    public void onCancel() {
      // no-op
    }
  }
}
